//
//  ID3v2Tag.m
//  MantuPlay
//
//  Created by Markus Sintonen on 12.4.2009.
//  Copyright 2009 __MyCompanyName__. All rights reserved.
//

#import "ID3TagReader.h"
#import "ID3TagGenres.h"
#import <assert.h>
#import <unistd.h>
#import <sys/types.h>
#import <sys/stat.h>
#import <fcntl.h>
#import "NSStringExtensions.h"

#define ID3v2_HEADER_LENGTH 10
#define ID3v2_FOOTER_LENGTH 10
#define ID3v2_EXTENDEDHEADER_SIZE_LENGTH 4

@implementation ID3TagReader

@synthesize title, artist, album, genre, trackNumber, albumArtData, partOfSet;

-(id)initWithFile:(const char *)file framesToRead:(int)frameTypes{
	self = [super init];
	if (self != nil) {
		self.albumArtData = [NSData data];
		
		frameTypesToRead = frameTypes;

		int fileHandle = open(file, O_RDONLY);
		if(fileHandle < 0) {
			NSLog(@"Error opening file");
			assert(0);
			return nil;
		}
		
		unsigned int totalHeaderLength = ID3v2_HEADER_LENGTH+ID3v2_EXTENDEDHEADER_SIZE_LENGTH;
		
		unsigned char *tagHeaderData = (unsigned char*)malloc(sizeof(unsigned char)*totalHeaderLength);
		if(tagHeaderData == NULL) {
			NSLog(@"Error allocating tagHeaderData");
			assert(0);
			return nil;
		}
		
		int readResult = read(fileHandle, tagHeaderData, totalHeaderLength);
		if(readResult != (int)totalHeaderLength) {
			NSLog(@"Error reading tagHeaderData");
			assert(0);
			return nil;
		}
		
		if(![self readHeaderFromData:tagHeaderData]) return nil;
		
		free(tagHeaderData);
		
		unsigned char *tagFramesData = (unsigned char*)malloc(sizeof(unsigned char)*framesDataSize);
		if(tagFramesData == NULL) {
			NSLog(@"Error allocating tagFramesData");
			assert(0);
			return nil;
		}
		
		readResult = pread(fileHandle, tagFramesData, framesDataSize, framesDataPosition);
		if(readResult != (int)framesDataSize) {
			NSLog(@"Error reading tagFramesData");
			assert(0);
			return nil;
		}
		
		close(fileHandle);
		
		if(usesUnsynchronisation && (majorVersion <= 3)) 
			framesDataSize = [ID3TagReader convertToUnsynchronizedData:tagFramesData dataLength:framesDataSize];
		
		[self parseFramesData:tagFramesData];
		
		free(tagFramesData);
	}
	return self;
}
-(void)parseFramesData:(unsigned char*)data {
	unsigned int currentFramePosition = 0;
	unsigned int frameHeaderSize = [ID3TagReader frameHeaderSizeForTagVersion:majorVersion];
	
	while(currentFramePosition < (framesDataSize - frameHeaderSize)) {
		
		//Padding encountered
		if(data[currentFramePosition] == 0) {
			return;
		}
		
		char frameID[4];
		unsigned int frameSize;
		unsigned char frameFlags[2];
		
		if(majorVersion < 3) {
			memcpy(frameID, data+currentFramePosition, 3);
			frameSize = [ID3TagReader getUInt:data+currentFramePosition+3 length:3];
		}
		else {
			memcpy(frameID, data+currentFramePosition, 4);
			if(majorVersion > 3) frameSize = [ID3TagReader getUnsynchsafeUInt:data+currentFramePosition+4];
			else frameSize = [ID3TagReader getUInt:data+currentFramePosition+4];
			frameFlags[0] = data[currentFramePosition + 8];
			frameFlags[1] = data[currentFramePosition + 9];
			
			//ITUNES HACK: iTunes writes v2.4 tags with v2.3-like frame sizes
			if(majorVersion > 3) {
				if(frameSize > 127) {
					if(![ID3TagReader isValidFrameID:data+currentFramePosition+10+frameSize]) {
						unsigned int uintSize = [ID3TagReader getUInt:data+currentFramePosition+4];
						if([ID3TagReader isValidFrameID:data+currentFramePosition+10+uintSize]) {
							frameSize = uintSize;
						}
					}
				}
			}
		}
		
		FrameType frameType = [ID3TagReader getFrameTypeFromFrameID:frameID tagVersion:majorVersion];
		
		if((frameType != NotSupportedType) && (frameType & frameTypesToRead)) {
			unsigned char *frameData = (unsigned char*)malloc(sizeof(unsigned char) * frameSize);
			memcpy(frameData, data+currentFramePosition+frameHeaderSize, sizeof(unsigned char) * frameSize);
			
			BOOL compressedFrame = FALSE;
			BOOL encryptedFrame = FALSE;
			if(majorVersion > 3) {
				compressedFrame = (BOOL)(frameFlags[1] & 0x8);
				encryptedFrame = (BOOL)(frameFlags[1] & 0x4);
				BOOL unsynchronizedFrame = (BOOL)(frameFlags[1] & 0x2);
				if(unsynchronizedFrame)
					frameSize = [ID3TagReader convertToUnsynchronizedData:frameData dataLength:frameSize];
			}
			else if(majorVersion == 3) {
				compressedFrame = (BOOL)(frameFlags[1] & 0x80);
				encryptedFrame = (BOOL)(frameFlags[1] & 0x40);
			}

			if((compressedFrame == FALSE) && (encryptedFrame == FALSE)) {
				switch (frameType) {
					case TitleType:
						self.title = [ID3TagReader stringFromTextFrameData:frameData length:frameSize];
						break;
					case ArtistType:
						self.artist = [ID3TagReader stringFromTextFrameData:frameData length:frameSize];
						break;
					case AlbumType:
						self.album = [ID3TagReader stringFromTextFrameData:frameData length:frameSize];
						break;
					case GenreType:
						self.genre = [ID3TagReader updateGenreFromGenreString:[ID3TagReader stringFromTextFrameData:frameData length:frameSize]];
						break;
					case TrackNumberType:
						self.trackNumber = [ID3TagReader updateTrackNumberFromTrackNumberString:[ID3TagReader stringFromTextFrameData:frameData length:frameSize]];
						break;
					case AlbumArtType:
						self.albumArtData = [ID3TagReader imageDataFromFrameData:frameData length:frameSize];
						break;
					case PartOfSetType:
						self.partOfSet = [ID3TagReader updateTrackNumberFromTrackNumberString:[ID3TagReader stringFromTextFrameData:frameData length:frameSize]];
						break;
					default:
						break;
				}
			}
			free(frameData);
		}
		currentFramePosition += frameHeaderSize + frameSize;
	}
}

-(BOOL)readHeaderFromData:(unsigned char*)data {
	if(![ID3TagReader findHeader:data]) {
		return FALSE;
	}
	
	majorVersion = data[3];
	
	unsigned char flags = data[5];
	usesUnsynchronisation  = (BOOL)(flags & 0x80);
	BOOL containsExtendedHeader = (BOOL)(flags & 0x40);
	
	framesDataSize = [ID3TagReader getUnsynchsafeUInt:&data[6]];
	framesDataPosition = ID3v2_HEADER_LENGTH;
	
	if(containsExtendedHeader) {
		unsigned int extendedHeaderSize = [ID3TagReader getUnsynchsafeUInt:data+ID3v2_HEADER_LENGTH];
		framesDataSize -= extendedHeaderSize;
		framesDataPosition += extendedHeaderSize;
	}
	
	return TRUE;
}

+(BOOL)findHeader:(unsigned char*)data {
	if(data[0] != 0x49) return FALSE;	//'I'
	if(data[1] != 0x44) return FALSE;	//'D'
	if(data[2] != 0x33) return FALSE;	//'3'
	if(data[3] > 0x04)	return FALSE;	//major version greater than 4 not supported
	return TRUE;
}

+(NSString*)stringFromTextFrameData:(unsigned char*)data length:(unsigned int)len {
	int encoding = data[0];
	
	NSStringEncoding strEncoding;
	switch (encoding) {
		case 0:
			strEncoding = NSISOLatin1StringEncoding;
			break;
		case 1:
			strEncoding = NSUnicodeStringEncoding;
			break;
		case 2:
			strEncoding = NSUnicodeStringEncoding;
			break;
		case 3:
			strEncoding = NSUTF8StringEncoding;
			break;
		default:
			strEncoding = NSISOLatin1StringEncoding;
			break;
	}
	
	//unsigned int textDelimiterPosition = len - 1;
	/*for(unsigned int i = 1; i < (len - 1); i++) {
		if(data[i] == 0x00) {
			if(strEncoding == NSUnicodeStringEncoding) {
				if(data[i + 1] == 0x00) {
					textDelimiterPosition = i;
					break;
				}
			}
			else {
				textDelimiterPosition = i;
				break;
			}
		}
	}*/

	NSString *str = [[[NSString alloc] initWithBytes:data+1 length:len-1 encoding:strEncoding] autorelease];
	//NSString *str = [[[NSString alloc] initWithCString:(char*)(data+1) encoding:strEncoding] autorelease];
	return [[str stringByTrimmingCharactersInSet:[NSCharacterSet controlCharacterSet]] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
}
+(NSString*)updateGenreFromGenreString:(NSString*)genreStr {
	if([genreStr characterAtIndex:0] == '(') {
		int closing = [genreStr findCharacter:')'];
		if(closing != -1) {
			int num = [[genreStr substringWithRange:NSMakeRange(1, closing - 1)] intValue];
			return genresList[num];
		}
	}
	return genreStr;
}
+(NSNumber*)updateTrackNumberFromTrackNumberString:(NSString*)trackNumberStr {
	/*int slashPos = [trackNumberStr findCharacter:'/'];
	if(slashPos != -1) {
		int num = [[trackNumberStr substringWithRange:NSMakeRange(0, slashPos - 1)] intValue];
		return [NSNumber numberWithUnsignedInt:(unsigned int)num];
	}*/
	return [NSNumber numberWithInt:[trackNumberStr intValue]];
}
+(NSData*)imageDataFromFrameData:(unsigned char*)data length:(unsigned int)len {
	unsigned int binaryDataPos = 0;
	int encoding = data[binaryDataPos]; //Picture description text encoding
	binaryDataPos++;
	while(binaryDataPos < len) { //We just ignore and skip picture mime type
		if(data[binaryDataPos] == 0) break;
		binaryDataPos++;
	}
	binaryDataPos++;
	binaryDataPos++; //We just ignore and skip picture type;
	while(binaryDataPos < len) { //We just ignore and skip picture description
		if(data[binaryDataPos] == 0) {
			if(encoding == 1 || encoding == 2) {
				if(data[binaryDataPos] == 0) {
					binaryDataPos++;
					break;
				}
			}
			break;
		}
		binaryDataPos++;
	}
	binaryDataPos++;
	if(binaryDataPos >= len) return [NSData data];

	return [NSData dataWithBytes:data+binaryDataPos length:len-binaryDataPos];
}

+(unsigned int)getUInt:(unsigned char*)data {
	unsigned int sum = 0;
	sum |= data[0] << 24;
	sum |= data[1] << 16;
	sum |= data[2] << 8;
	sum |= data[3];
	
	return sum;
}
+(unsigned int)getUInt:(unsigned char*)data length:(unsigned int)len{
	if(len >= sizeof(unsigned int)) return [ID3TagReader getUInt:data];
	
    unsigned int sum = 0;
    unsigned int last = len - 1;
	
    for(unsigned int i = 0; i <= last; i++)
		sum |= (unsigned int)data[i] << ((last - i) * 8);
	
    return sum;
}
+(unsigned int)getUnsynchsafeUInt:(unsigned char*)data {
	unsigned int sum = 0;
	sum |= (data[0] & 0x7f) << 21;
	sum |= (data[1] & 0x7f) << 14;
	sum |= (data[2] & 0x7f) << 7;
	sum |= (data[3] & 0x7f);
	
	return sum;
}
+(unsigned int)getUnsynchsafeUInt:(unsigned char*)data length:(unsigned int)len {
	if(len >= sizeof(unsigned int)) return [ID3TagReader getUnsynchsafeUInt:data];
	
	int last = len - 1;
	unsigned int sum = 0;
	
	for(int i = 0; i <= last; i++)
	 sum |= (data[i] & 0x7f) << ((last - i) * 7);
	
	return sum;
}
+(unsigned int)convertToUnsynchronizedData:(unsigned char*)data dataLength:(unsigned int)len {
    int shift = 0;
	
	unsigned char *unsynced = (unsigned char*)malloc(sizeof(unsigned char) * len);
	memcpy(unsynced, data, sizeof(unsigned char) * len);
	
    for( unsigned int i = 1; i < len; i++ )
    {
        if(data[i - 1] == 0xFF && data[i] == 0x00)
            shift++;
        else if(shift)
            unsynced[i - shift] = data[i];
    }
	
	unsigned int newLength = len - shift;
	
    if(shift) {
		free(data);
		data = (unsigned char*)malloc(sizeof(unsigned char) * newLength);
		memcpy(data, unsynced, sizeof(unsigned char) * newLength);
	}
		
	free(unsynced);
	
	return newLength;
}

+(FrameType)getFrameTypeFromFrameID:(char*)frameID tagVersion:(int)version {
	if(memcmp(frameID, "T", 1) == 0) { //Is text frame
		if(version < 3) {
			if(memcmp(frameID + 1, "T2", 2) == 0) return TitleType;
			else if(memcmp(frameID + 1, "P1", 2) == 0) return ArtistType;
			else if(memcmp(frameID + 1, "AL", 2) == 0) return AlbumType;
			else if(memcmp(frameID + 1, "CO", 2) == 0) return GenreType;
			else if(memcmp(frameID + 1, "RK", 2) == 0) return TrackNumberType;
			else if(memcmp(frameID + 1, "PA", 2) == 0) return PartOfSetType;
			else return NotSupportedType;
		}
		else {
			if(memcmp(frameID + 1, "IT2", 3) == 0) return TitleType;
			else if(memcmp(frameID + 1, "PE1", 3) == 0) return ArtistType;
			else if(memcmp(frameID + 1, "ALB", 3) == 0) return AlbumType;
			else if(memcmp(frameID + 1, "CON", 3) == 0) return GenreType;
			else if(memcmp(frameID + 1, "RCK", 3) == 0) return TrackNumberType;
			else if(memcmp(frameID + 1, "POS", 3) == 0) return PartOfSetType;
			else return NotSupportedType;
		}
	}
	if(version < 3) {
		if(memcmp(frameID, "PIC", 3) == 0) return AlbumArtType;
	}
	else {
		if(memcmp(frameID, "APIC", 4) == 0) return AlbumArtType;
	}
	
	return NotSupportedType;
}
+(BOOL)isValidFrameID:(unsigned char*)data {
	for(unsigned int i = 0; i < 4; i++) {
		char c = data[i];
		if((c < 'A' || c > 'Z') && (c < '1' || c > '9')) return FALSE;
	}
	return TRUE;
}

+(unsigned int)frameHeaderSizeForTagVersion:(unsigned int)version
{
	switch(version) {
		case 0:
		case 1:
		case 2:
			return 6;
		case 3:
		case 4:
		default:
			return 10;
	}
}

- (void)dealloc {
	[title release];
	[artist release];
	[album release];
	[genre release];
	[trackNumber release];
	[albumArtData release];
	[partOfSet release];
	
	majorVersion = NULL;
	usesUnsynchronisation = NULL;
	framesDataSize = NULL;
	framesDataPosition = NULL;
	
    [super dealloc];
}
@end